想像你是一位經驗豐富的建築師,精通鋼筋混凝土的現代建築工法。現在,你來到了日本,準備學習傳統的木造建築技術。你會發現,雖然都是建造房屋,但整個思維方式完全不同。木造建築不依賴釘子,而是透過精巧的榫卯結構讓每個部件完美契合。這就像從其他程式語言轉換到 Ruby 和 Rails 的體驗。
如果你來自 JavaScript 的世界,你習慣了事件驅動和非同步的思維模式,在 Express.js 中用中介軟體串連請求處理流程。如果你是 Java 開發者,你依賴強型別系統提供的安全網,在 Spring Boot 中用註解定義一切行為。Python 開發者則享受著簡潔的語法,在 FastAPI 中用型別提示來自動生成文檔。
今天,我們要開始探索 Ruby 這個獨特的世界。Ruby 的創造者松本行弘說過,Ruby 的設計目標是「讓程式設計師快樂」。這不是一句空洞的口號,而是深深烙印在語言每個細節中的設計理念。當你理解了 Ruby 的 blocks、procs、lambdas 和 symbols,你會發現 Rails 那些看似魔法的功能,其實都有著清晰而優雅的實現原理。
更重要的是,今天我們不會停留在抽象的語法討論。從第一刻起,我們就會在真實的 Rails 環境中探索這些特性,並且開始建構我們的線上學習管理系統(LMS)。這個專案將陪伴我們整個三十天的學習旅程,逐步成長為一個完整的生產級系統。每一天的學習都是為這個最終目標添磚加瓦。
在我們深入 Ruby 的優雅設計之前,需要先準備好開發環境。這個過程就像畫家在創作前需要準備畫布和顏料一樣重要。不同的是,我們的畫布是終端機,顏料是程式碼。
Ruby 社群很早就意識到版本管理的重要性。不同的專案可能需要不同版本的 Ruby,就像不同的菜餚需要不同的烹飪溫度。rbenv 就是我們的溫度控制器,讓我們能在不同專案間自如切換。這個概念對 Node.js 開發者來說就像 nvm,對 Python 開發者來說就像 pyenv。
讓我們開始安裝過程。首先安裝 rbenv 和 Ruby。如果你使用 macOS,過程會透過 Homebrew 進行。打開終端機,輸入以下命令,每一行都有其特定的用途:
# macOS 使用者的安裝流程
brew install rbenv ruby-build # 安裝 rbenv 和它的編譯助手
rbenv init # 初始化 rbenv 環境
echo 'eval "$(rbenv init -)"' >> ~/.zshrc # 讓 rbenv 在每次開啟終端時自動啟動
source ~/.zshrc # 立即載入新的設定
# Linux (Ubuntu/Debian) 使用者的安裝流程
# 首先安裝必要的系統套件,這些是編譯 Ruby 所需的
sudo apt update
sudo apt install git curl libssl-dev libreadline-dev zlib1g-dev \
autoconf bison build-essential libyaml-dev \
libreadline-dev libncurses5-dev libffi-dev libgdbm-dev
# 使用官方安裝腳本
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash
# 設定環境變數,確保系統能找到 rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
現在我們可以安裝 Ruby 了。我們選擇 3.2.2 版本,這是 Rails 7.1 推薦的穩定版本:
rbenv install 3.2.2 # 這會需要幾分鐘,Ruby 會從原始碼編譯
rbenv global 3.2.2 # 設定為預設版本
ruby -v # 確認安裝成功,應該顯示 ruby 3.2.2
# 安裝 Rails
gem install rails -v 7.1.3
rails -v # 確認 Rails 版本
現在來到激動人心的時刻,我們要創建真正的專案了。這不是練習用的玩具,而是接下來三十天我們要持續發展的系統。每個參數都有其深意:
# 創建 Rails API 專案
rails new lms_api --api --database=postgresql -T
# 讓我解釋每個參數的含義:
# lms_api: 我們的專案名稱,代表 Learning Management System API
# --api: 告訴 Rails 我們只需要 API 功能,不需要視圖層組件
# --database=postgresql: 使用 PostgreSQL 而非預設的 SQLite,因為這更接近生產環境
# -T: 暫時跳過測試框架,因為我們之後會使用 RSpec 而非預設的 Minitest
cd lms_api
讓我們花點時間理解 Rails 為我們創建的專案結構。每個資料夾都像是一個專門的工作間,有其特定的職責:
# 查看專案結構
ls -la
# 你會看到這樣的結構,讓我解釋每個部分的用途:
# app/ # 應用程式的核心,包含所有業務邏輯
# ├── controllers/ # 處理 HTTP 請求的控制器
# ├── models/ # 資料模型和業務邏輯
# └── ...
# config/ # 所有配置文件的家
# ├── routes.rb # 定義 URL 與控制器動作的對應關係
# ├── database.yml # 資料庫連接設定
# └── ...
# db/ # 資料庫相關文件
# Gemfile # Ruby 的 package.json,定義專案依賴
在開始寫程式之前,我們需要設置資料庫。如果你還沒有安裝 PostgreSQL,Docker 提供了一個快速的解決方案:
# 使用 Docker 快速啟動 PostgreSQL
docker run --name lms_postgres \
-e POSTGRES_PASSWORD=password \
-p 5432:5432 \
-d postgres:14
# 創建開發和測試資料庫
rails db:create
# 啟動 Rails 伺服器來確認一切正常
rails server
# 在瀏覽器訪問 http://localhost:3000
# 你應該會看到 Rails 的歡迎頁面
Rails Console 是一個神奇的學習環境。它不只是一個 REPL(Read-Eval-Print Loop),而是載入了整個 Rails 應用環境的互動式工作台。在這裡,你可以即時實驗任何想法,立即看到結果。這就像是一個配備了所有工具的實驗室,讓你能安全地探索和學習。
rails console # 或簡寫為 rails c
在 Ruby 的世界觀中,真的是「一切皆物件」。這不是一種比喻或誇飾,而是字面上的事實。每個你能想到的東西,包括數字、字串、甚至 nil
和 true
,都是完整的物件,擁有自己的方法和屬性。
讓我們在 console 中親身體驗這個概念。當你輸入這些程式碼時,請仔細觀察每個結果,思考它們的含義:
# 數字是物件,而且是功能豐富的物件
5.class # => Integer
5.methods.count # => 134(這個簡單的數字 5 有超過一百個方法!)
# 讓我們看看數字能做什麼
5.times { |i| puts "第 #{i + 1} 次迴圈" } # 數字知道如何重複執行程式碼
5.even? # => false(數字知道自己是奇數還是偶數)
5.next # => 6(數字知道下一個數字是什麼)
# 字串當然也是物件
"Hello".class # => String
"Hello".upcase # => "HELLO"
"Hello".reverse # => "olleH"
"Hello".include?("ll") # => true
# 更令人驚訝的是,連 nil、true、false 都是物件
nil.class # => NilClass
true.class # => TrueClass
false.class # => FalseClass
nil.to_s # => ""(nil 知道如何轉換成字串)
true.to_i # => 1(true 知道如何轉換成整數)
# Rails 擴展了這個概念,讓程式碼讀起來像自然語言
require 'active_support/all' # 如果在純 Ruby 環境中需要載入
2.days.ago # => 2 天前的時間物件
3.megabytes # => 3145728(位元組數)
"user".pluralize # => "users"(自動複數化)
"users".singularize # => "user"(自動單數化)
這種設計哲學深深影響了 Rails 的 API 設計。當你寫 user.save
而不是 save(user)
時,你是在告訴物件去執行動作,而不是對物件執行操作。這種細微的差別造就了 Rails 程式碼的優雅和直觀。物件不是被動的資料容器,而是主動的參與者。
Blocks 是 Ruby 最獨特也最強大的特性之一。如果說物件是 Ruby 的身體,那麼 Blocks 就是它的靈魂。Block 不只是匿名函數那麼簡單,它是一種優雅的控制流程方式,讓你能夠將程式碼片段像參數一樣傳遞。
讓我們從最簡單的例子開始,逐步深入理解 Blocks 的本質:
# 最基本的 Block:使用大括號的單行形式
[1, 2, 3].each { |number| puts number * 2 }
# 多行 Block:使用 do...end(這是 Rails 的慣例)
[1, 2, 3].each do |number|
# Block 內可以有多行程式碼
result = number * 2
puts "#{number} 的兩倍是 #{result}"
end
# Block 最神奇的特性:閉包(Closure)
# Block 能夠「記住」它被定義時的環境
multiplier = 10
results = [1, 2, 3].map { |n| n * multiplier }
puts results # => [10, 20, 30]
# 即使離開了原本的作用域,Block 仍然記得 multiplier
def create_multiplier(factor)
# 返回一個 Proc(稍後會詳細解釋)
lambda { |n| n * factor }
end
times_five = create_multiplier(5)
puts times_five.call(3) # => 15(記住了 factor = 5)
現在讓我們創建自己的方法來理解 Block 的運作機制。這個例子會展示 Rails 中常見的模式:
# 定義一個接受 Block 的方法
def measure_time(operation_name = "操作")
puts "開始 #{operation_name}..."
start_time = Time.now
# yield 是關鍵字,它執行傳入的 block
result = yield if block_given? # block_given? 檢查是否有提供 block
end_time = Time.now
duration = end_time - start_time
puts "#{operation_name} 完成!"
puts "耗時:#{duration.round(3)} 秒"
result # 返回 block 的執行結果
end
# 使用我們的計時器方法
total = measure_time("計算總和") do
sum = 0
1_000_000.times { |i| sum += i }
sum # block 的最後一行是返回值
end
puts "計算結果:#{total}"
# 這個模式在 Rails 中無處不在
# 例如,資料庫交易:
# ActiveRecord::Base.transaction do
# user.save!
# account.save!
# end
Symbol 可能是 Ruby 中最讓新手困惑的概念,但它也是理解 Rails 的關鍵。讓我用一個類比來解釋:想像你在管理一個大型圖書館。每本書都需要一個識別方式,你有兩個選擇。第一種是每次都寫下完整的書名(這就像 String),第二種是給每本書一個永久的編號,之後都用編號來指稱(這就像 Symbol)。
Symbol 的本質是「名稱的名稱」,它代表的是標識本身,而不是可變的內容:
# 首先,讓我們理解 Symbol 的唯一性
# 每次使用相同的 Symbol,都指向同一個物件
:name.object_id # 比如:1086748
:name.object_id # 還是:1086748
:name.object_id # 永遠:1086748
# 相比之下,String 每次都創建新物件
"name".object_id # 比如:70234567890
"name".object_id # 變成:70234567920
"name".object_id # 又變:70234567950
# 這個差異看似微小,但影響巨大
# 讓我們做個實驗來理解記憶體影響
def compare_memory_usage
# 測試 String
string_array = []
10_000.times { string_array << "status" }
# 測試 Symbol
symbol_array = []
10_000.times { symbol_array << :status }
puts "10,000 個 'status' String 建立了 10,000 個物件"
puts "10,000 個 :status Symbol 只引用 1 個物件"
end
compare_memory_usage
Symbol 在 Rails 中無處不在,理解它們的使用場景對於寫出地道的 Rails 程式碼至關重要:
# Hash 鍵:Symbol 的最常見用途
# 為什麼偏好 Symbol 作為 Hash 鍵?
# 使用 String 作為鍵(較慢,佔用更多記憶體)
user_with_strings = {
"name" => "Alice",
"email" => "alice@example.com",
"role" => "admin"
}
# 使用 Symbol 作為鍵(較快,記憶體效率高)
user_with_symbols = {
name: "Alice", # 注意這個語法糖,等同於 :name => "Alice"
email: "alice@example.com",
role: "admin"
}
# 效能差異測試
require 'benchmark'
n = 1_000_000
Benchmark.bm(20) do |x|
x.report("String keys 查詢:") do
n.times { user_with_strings["name"] }
end
x.report("Symbol keys 查詢:") do
n.times { user_with_symbols[:name] }
end
end
# Symbol 通常快 20-30%,因為 Symbol 的比較是指標比較
但這裡有一個重要的陷阱需要注意,這是很多 Rails 新手會踩的坑:
# Symbol 與資料庫值的型別差異
class Course < ApplicationRecord
# 在 Model 定義中,我們到處使用 Symbol
validates :title, presence: true
scope :published, -> { where(status: :published) }
end
# 建立課程時,我們可以使用 Symbol
course = Course.new
course.status = :published # 可以賦值 Symbol
course.save
# 但是!從資料庫讀取時...
course = Course.first
puts course.status.class # => String!不是 Symbol!
# 這意味著比較時要小心
course.status == :published # => false(String != Symbol)
course.status == "published" # => true
course.status.to_sym == :published # => true
# Rails 提供了優雅的解決方案:Enum
class Course < ApplicationRecord
enum status: {
draft: 0,
published: 1,
archived: 2
}
end
# 現在 Rails 會自動處理型別轉換
course.published? # => true/false(自動生成的方法)
course.published! # 設定狀態為 published
Course.published # 返回所有已發布的課程(自動生成的 scope)
最後,讓我分享一個關於 Symbol 的重要安全知識。這是一個許多開發者不知道,但可能造成嚴重問題的細節:
# 危險:Symbol 一旦創建就永遠不會被垃圾回收!
# 這個特性可能被利用來進行記憶體攻擊
def dangerous_method(user_input)
# 永遠不要這樣做!
user_input.to_sym # 如果用戶不斷傳入不同的字串,會耗盡記憶體
end
# 安全的做法:使用白名單
ALLOWED_ACTIONS = %i[create read update delete].freeze
def safe_method(user_input)
# 只轉換已知安全的值
if ALLOWED_ACTIONS.map(&:to_s).include?(user_input)
action = user_input.to_sym
perform_action(action)
else
raise "不允許的操作"
end
end
# 或者更好:完全避免動態 Symbol 轉換
def safest_method(user_input)
case user_input
when "create" then create_something
when "read" then read_something
when "update" then update_something
when "delete" then delete_something
else
raise "不允許的操作"
end
end
當 Block 需要被儲存、傳遞或重複使用時,我們就需要將它物件化。Ruby 提供了兩種方式:Proc 和 Lambda。它們就像是 Block 的容器,讓 Block 能夠像普通物件一樣被操作。
# Proc 是 Block 的基本物件化形式
say_hello = Proc.new { |name| puts "Hello, #{name}!" }
say_hello.call("Ruby") # => Hello, Ruby!
say_hello.("Rails") # => Hello, Rails!(語法糖)
say_hello["World"] # => Hello, World!(另一種語法糖)
# Lambda 是更嚴格的 Proc
greet = lambda { |name| puts "Greetings, #{name}!" }
greet.call("Developer")
# Lambda 的現代語法(Ruby 1.9+)
modern_greet = ->(name) { puts "Hi, #{name}!" }
modern_greet.call("Friend")
# 多參數的 Lambda
calculate = ->(a, b, operation) {
case operation
when :add then a + b
when :multiply then a * b
else raise "未知操作"
end
}
puts calculate.call(5, 3, :add) # => 8
Proc 和 Lambda 看起來很相似,但有兩個關鍵差異。理解這些差異對於選擇正確的工具很重要:
# 差異一:參數檢查的嚴格程度
my_proc = Proc.new { |a, b| puts "Proc: a=#{a}, b=#{b}" }
my_lambda = lambda { |a, b| puts "Lambda: a=#{a}, b=#{b}" }
# Proc 對參數數量很寬鬆
my_proc.call(1) # 輸出 "Proc: a=1, b="(b 是 nil)
my_proc.call(1, 2, 3) # 輸出 "Proc: a=1, b=2"(忽略多餘的 3)
# Lambda 嚴格檢查參數數量
# my_lambda.call(1) # ArgumentError: wrong number of arguments
# my_lambda.call(1, 2, 3) # ArgumentError: wrong number of arguments
# 差異二:return 的行為
def test_proc_return
my_proc = Proc.new { return "從 Proc 返回" }
my_proc.call
"這行不會執行" # Proc 的 return 會從方法返回
end
def test_lambda_return
my_lambda = lambda { return "從 Lambda 返回" }
result = my_lambda.call
"Lambda 返回後繼續執行,結果是:#{result}" # 這行會執行
end
puts test_proc_return # => "從 Proc 返回"
puts test_lambda_return # => "Lambda 返回後繼續執行,結果是:從 Lambda 返回"
現在我們已經理解了 Ruby 的核心概念,是時候在 Rails 中應用這些知識了。讓我們建立真實的 Model 和 Controller,看看這些概念如何在實際開發中發揮作用。
首先,我們要為 LMS 系統建立基礎的資料模型。Rails 的 generator 會幫我們生成必要的檔案:
# 退出 console(輸入 exit)
# 建立課程、章節、課時的 Model
rails generate model Course title:string description:text status:string
rails generate model Chapter course:references title:string position:integer
rails generate model Lesson chapter:references title:string duration:integer content_type:string
# 執行資料庫遷移,建立實際的資料表
rails db:migrate
# 重新進入 console 來探索我們的 Model
rails c
現在讓我們強化這些 Model,加入 Ruby 和 Rails 的特性:
# app/models/course.rb
class Course < ApplicationRecord
# 關聯定義:使用 Symbol 指定關聯名稱和選項
has_many :chapters, dependent: :destroy # 刪除課程時連帶刪除章節
has_many :lessons, through: :chapters # 透過章節間接關聯課時
# Validations:確保資料完整性
validates :title, presence: true, length: { minimum: 3, maximum: 100 }
validates :description, presence: true, length: { minimum: 10 }
validates :status, inclusion: {
in: %w[draft published archived],
message: "%{value} 不是有效的狀態"
}
# Callbacks:在特定時機執行程式碼
before_save :normalize_title
after_create :log_creation
before_validation :set_default_status, on: :create
# Scopes:使用 Lambda 定義可重用的查詢
scope :published, -> { where(status: 'published') }
scope :draft, -> { where(status: 'draft') }
scope :recent, -> { order(created_at: :desc) }
scope :with_chapters, -> { includes(:chapters) } # 預載入,避免 N+1 查詢
# 自定義方法
def publish!
update(status: 'published', published_at: Time.current)
end
def published?
status == 'published'
end
def total_duration
lessons.sum(:duration)
end
private
def normalize_title
# 使用 Ruby 的字串方法來標準化標題
self.title = title.strip.titleize if title.present?
end
def set_default_status
self.status ||= 'draft'
end
def log_creation
Rails.logger.info "新課程建立:#{title} (ID: #{id})"
end
end
讓我們在 console 中測試這些功能,看看 Ruby 特性如何讓操作變得直觀:
# 建立課程,注意 Symbol 作為 Hash 鍵
course = Course.create(
title: " ruby on rails 基礎 ", # 會被自動標準化
description: "這是一門完整的 Rails 入門課程,適合初學者"
)
puts course.title # => "Ruby On Rails 基礎"(自動標準化了!)
puts course.status # => "draft"(預設值)
# 使用 scope 進行查詢
Course.published.recent.each do |c|
puts "#{c.title} - 發布於 #{c.published_at}"
end
# Block 在批次操作中的應用
Course.draft.find_each do |course|
# find_each 會分批載入,避免記憶體問題
puts "處理草稿課程:#{course.title}"
# 可以在這裡執行批次操作
end
現在讓我們建立完整的 API Controller,這裡會大量使用我們學到的 Ruby 特性:
# app/controllers/api/v1/courses_controller.rb
module Api
module V1
class CoursesController < ApplicationController
# before_action 使用 Symbol 指定方法名和動作
before_action :set_course, only: [:show, :update, :destroy, :publish]
# GET /api/v1/courses
def index
# 使用 scope 和 includes 優化查詢
@courses = Course.published.recent.includes(:chapters)
# 分頁處理
page = params[:page] || 1
per_page = params[:per_page] || 10
@courses = @courses.page(page).per(per_page)
# 使用 Block 來格式化回應
render json: {
courses: @courses.map { |course| serialize_course(course) },
meta: pagination_meta(@courses)
}
end
# GET /api/v1/courses/:id
def show
# 使用 Lambda 來定義序列化邏輯
detailed_serializer = ->(course) {
{
id: course.id,
title: course.title,
description: course.description,
status: course.status,
total_duration: course.total_duration,
chapters: course.chapters.map { |chapter|
{
id: chapter.id,
title: chapter.title,
position: chapter.position,
lessons_count: chapter.lessons.count
}
},
created_at: course.created_at,
updated_at: course.updated_at
}
}
render json: detailed_serializer.call(@course)
end
# POST /api/v1/courses
def create
@course = Course.new(course_params)
# 使用交易確保資料一致性
ActiveRecord::Base.transaction do
if @course.save
# 如果有章節資料,一併建立
create_chapters if params[:chapters].present?
render json: {
status: 'success',
data: serialize_course(@course),
message: '課程建立成功'
}, status: :created
else
# 交易會自動回滾
render json: {
status: 'error',
errors: @course.errors.full_messages
}, status: :unprocessable_entity
end
end
end
# PUT/PATCH /api/v1/courses/:id
def update
if @course.update(course_params)
render json: {
status: 'success',
data: serialize_course(@course),
message: '課程更新成功'
}
else
render json: {
status: 'error',
errors: @course.errors.full_messages
}, status: :unprocessable_entity
end
end
# DELETE /api/v1/courses/:id
def destroy
@course.destroy
head :no_content # 返回 204 No Content
end
# PUT /api/v1/courses/:id/publish
def publish
if @course.publish!
render json: {
status: 'success',
message: '課程已發布',
published_at: @course.published_at
}
else
render json: {
status: 'error',
message: '課程發布失敗'
}, status: :unprocessable_entity
end
end
private
# Strong Parameters:使用 Symbol 指定允許的參數
def course_params
params.require(:course).permit(
:title,
:description,
:status,
chapters_attributes: [
:title,
:position,
lessons_attributes: [:title, :duration, :content_type]
]
)
end
def set_course
@course = Course.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: {
status: 'error',
message: '找不到指定的課程'
}, status: :not_found
end
def serialize_course(course)
{
id: course.id,
title: course.title,
description: course.description,
status: course.status,
chapters_count: course.chapters.count,
lessons_count: course.lessons.count,
total_duration: course.total_duration
}
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count,
per_page: collection.limit_value
}
end
def create_chapters
params[:chapters].each do |chapter_params|
@course.chapters.create(
title: chapter_params[:title],
position: chapter_params[:position]
)
end
end
end
end
end
Rails 的路由系統是 Ruby DSL 的絕佳範例。它展示了如何用 Block 和 Symbol 創造出既強大又易讀的配置語言:
# config/routes.rb
Rails.application.routes.draw do
# namespace 使用 Block 來組織路由
namespace :api do
namespace :v1 do
# resources 方法展現了 Rails 的約定優於配置
# 一行程式碼定義了七個 RESTful 路由
resources :courses do
# member 區塊:作用於單一資源的額外動作
member do
put :publish # PUT /api/v1/courses/:id/publish
put :archive # PUT /api/v1/courses/:id/archive
end
# collection 區塊:作用於資源集合的額外動作
collection do
get :published # GET /api/v1/courses/published
get :drafts # GET /api/v1/courses/drafts
get :search # GET /api/v1/courses/search
end
# 巢狀資源:表達資源間的階層關係
resources :chapters, only: [:index, :create, :update, :destroy] do
resources :lessons, only: [:index, :create, :update, :destroy]
end
end
# 也可以定義單一資源(沒有 ID)
resource :profile, only: [:show, :update]
end
end
# 使用 Lambda 實現動態路由約束
# 這展示了 Lambda 在 Rails 中的實際應用
constraints(lambda { |request|
# 只有特定 header 的請求才能訪問 v2 API
request.headers['API-Version'] == '2.0'
}) do
namespace :api do
namespace :v2 do
resources :courses
end
end
end
# 根路徑
root to: proc { [200, {}, ['LMS API Server Running']] }
end
讓我們測試這些 API 端點,看看我們的成果:
# 啟動伺服器
rails server
# 在另一個終端測試 API
# 建立新課程
curl -X POST http://localhost:3000/api/v1/courses \
-H "Content-Type: application/json" \
-d '{
"course": {
"title": "Rails API 開發完整指南",
"description": "從零開始學習如何使用 Rails 建構強大的 API"
}
}'
# 取得所有已發布的課程
curl http://localhost:3000/api/v1/courses
# 取得特定課程的詳細資訊
curl http://localhost:3000/api/v1/courses/1
# 發布課程
curl -X PUT http://localhost:3000/api/v1/courses/1/publish
Rails 的優雅很大程度上來自於它大量使用 DSL。DSL 讓複雜的邏輯變得像自然語言一樣易讀。現在讓我們為 LMS 系統建立一個課程建構 DSL,這會綜合運用今天學到的所有概念:
# app/services/course_builder.rb
class CourseBuilder
attr_reader :course
def initialize(title = nil)
@course = Course.new(title: title)
@current_chapter = nil
@chapter_position = 0
end
# DSL 方法:設定課程基本資訊
def title(text)
@course.title = text
self # 返回 self 以支援方法鏈
end
def description(text)
@course.description = text
self
end
def status(status_symbol)
@course.status = status_symbol.to_s
self
end
# DSL 方法:建立章節
def chapter(title, &block)
@chapter_position += 1
@current_chapter = @course.chapters.build(
title: title,
position: @chapter_position
)
# 如果提供了 block,在章節的上下文中執行
if block_given?
# instance_eval 讓 block 在當前物件的上下文中執行
# 這樣 block 內的方法調用會在 self 上執行
instance_eval(&block)
end
@current_chapter = nil # 重置當前章節
self
end
# DSL 方法:建立課時(必須在章節內)
def lesson(title, duration: 30, type: :video)
unless @current_chapter
raise "課時必須定義在章節內。請先使用 chapter 方法。"
end
@current_chapter.lessons.build(
title: title,
duration: duration,
content_type: type.to_s
)
self
end
# DSL 方法:新增教材
def material(name, url)
unless @current_chapter
raise "教材必須關聯到章節"
end
# 這裡可以建立教材關聯
# @current_chapter.materials.build(name: name, url: url)
self
end
# 儲存建構的課程
def save
@course.save
end
def save!
@course.save!
end
# 類別方法:提供更優雅的建構方式
def self.build(&block)
builder = new
builder.instance_eval(&block) if block_given?
builder.course
end
# 類別方法:建構並儲存
def self.create(&block)
builder = new
builder.instance_eval(&block) if block_given?
builder.save!
builder.course
end
end
# 使用我們的 DSL 來建構課程
# 注意這讀起來幾乎像自然語言!
course = CourseBuilder.create do
title "Ruby on Rails API 開發實戰"
description "30 天掌握 Rails API 開發的完整指南"
status :draft
chapter "Ruby 語言基礎" do
lesson "變數與資料型別", duration: 20
lesson "控制結構", duration: 25
lesson "物件導向程式設計", duration: 45, type: :video
lesson "Blocks、Procs 與 Lambdas", duration: 60, type: :video
end
chapter "Rails 框架入門" do
lesson "MVC 架構概述", duration: 30
lesson "路由系統詳解", duration: 35
lesson "控制器與動作", duration: 40
lesson "模型與資料庫", duration: 50
end
chapter "API 開發實戰" do
lesson "RESTful API 設計", duration: 45
lesson "認證與授權", duration: 55
lesson "錯誤處理", duration: 30
lesson "API 版本控制", duration: 35
lesson "效能優化", duration: 60
end
end
# 檢視建構的結果
puts "課程:#{course.title}"
puts "章節數:#{course.chapters.count}"
puts "總課時:#{course.lessons.count}"
puts "總時長:#{course.total_duration} 分鐘"
course.chapters.each do |chapter|
puts "\n章節 #{chapter.position}:#{chapter.title}"
chapter.lessons.each do |lesson|
puts " - #{lesson.title} (#{lesson.duration} 分鐘, #{lesson.content_type})"
end
end
Rails 鼓勵使用 Concerns 來封裝可重用的功能。讓我們建立一個狀態管理模組,展示如何運用 Ruby 的模組系統:
# app/models/concerns/statusable.rb
module Statusable
extend ActiveSupport::Concern
# 定義狀態常數(使用 Symbol 作為鍵)
STATUSES = {
draft: "草稿",
published: "已發布",
archived: "已封存"
}.freeze
# included 區塊:當模組被包含時執行
included do
# 定義 validations
validates :status, inclusion: {
in: STATUSES.keys.map(&:to_s),
message: "%{value} 不是有效的狀態"
}
# 定義 scopes(使用 Lambda)
scope :with_status, ->(status) { where(status: status.to_s) }
scope :published, -> { with_status(:published) }
scope :draft, -> { with_status(:draft) }
scope :archived, -> { with_status(:archived) }
# 定義 callbacks
before_validation :set_default_status, on: :create
# 如果有 published_at 欄位,自動設定
before_save :set_published_at, if: :publishing?
before_save :set_archived_at, if: :archiving?
end
# 類別方法
class_methods do
def status_options
STATUSES.map { |key, label| [label, key.to_s] }
end
def status_options_for_select
status_options
end
end
# 實例方法
def status_label
STATUSES[status.to_sym] if status.present?
end
def draft?
status == 'draft'
end
def published?
status == 'published'
end
def archived?
status == 'archived'
end
def publish!
update(status: 'published')
end
def archive!
update(status: 'archived')
end
def unpublish!
update(status: 'draft')
end
private
def set_default_status
self.status ||= 'draft'
end
def publishing?
status_changed? && status == 'published'
end
def archiving?
status_changed? && status == 'archived'
end
def set_published_at
self.published_at ||= Time.current if respond_to?(:published_at=)
end
def set_archived_at
self.archived_at ||= Time.current if respond_to?(:archived_at=)
end
end
# 在 Course Model 中使用
class Course < ApplicationRecord
include Statusable # 包含我們的模組
# 其他 Course 特定的邏輯...
end
# 現在 Course 擁有了所有狀態管理功能
course = Course.new(title: "新課程")
puts course.draft? # => true(預設狀態)
puts course.status_label # => "草稿"
course.publish!
puts course.published? # => true
puts course.published_at # => 2024-01-27 14:30:00
# 使用 scope
Course.published.count # 統計已發布的課程
Course.draft.each do |c|
puts "草稿課程:#{c.title}"
end
現在讓我們通過一個綜合練習來鞏固今天學到的知識。我們要建立一個批次處理服務,它會運用 Block、Lambda、Symbol 等所有概念:
# app/services/batch_processor.rb
class BatchProcessor
attr_reader :results, :errors
def initialize(model_class)
@model_class = model_class
@results = []
@errors = []
@filters = []
end
# 使用 Lambda 新增過濾條件
def add_filter(&block)
@filters << block if block_given?
self
end
# 使用 Block 處理每個記錄
def process(scope = @model_class.all, &block)
# 應用所有過濾條件
filtered_scope = @filters.reduce(scope) do |current_scope, filter|
current_scope.where(filter)
end
# 批次處理,避免記憶體問題
filtered_scope.find_each do |record|
begin
result = yield(record) if block_given?
@results << {
id: record.id,
status: :success,
result: result
}
rescue StandardError => e
@errors << {
id: record.id,
status: :error,
message: e.message,
backtrace: e.backtrace.first(3)
}
end
end
self
end
# 條件處理:只處理符合條件的記錄
def process_if(condition_lambda, &block)
process do |record|
if condition_lambda.call(record)
yield(record)
else
{ skipped: true, reason: "不符合條件" }
end
end
end
# 使用 Symbol 執行預定義的操作
def execute_action(action_symbol)
case action_symbol
when :publish
process { |record| record.publish! if record.respond_to?(:publish!) }
when :archive
process { |record| record.archive! if record.respond_to?(:archive!) }
when :delete
process { |record| record.destroy }
else
raise ArgumentError, "未知的操作:#{action_symbol}"
end
end
def success_count
@results.count { |r| r[:status] == :success }
end
def error_count
@errors.count
end
def summary
{
total_processed: @results.count + @errors.count,
successful: success_count,
failed: error_count,
success_rate: success_count.to_f / (success_count + error_count),
results: @results,
errors: @errors
}
end
end
# 使用範例
processor = BatchProcessor.new(Course)
# 新增過濾條件(使用 Lambda)
processor.add_filter { where(status: 'draft') }
processor.add_filter { where('created_at < ?', 1.week.ago) }
# 處理符合條件的記錄
processor.process do |course|
course.publish!
puts "發布課程:#{course.title}"
{ published_at: course.published_at }
end
# 檢視結果
summary = processor.summary
puts "處理完成!"
puts "成功:#{summary[:successful]} 個"
puts "失敗:#{summary[:failed]} 個"
puts "成功率:#{(summary[:success_rate] * 100).round(2)}%"
# app/services/permission_dsl.rb
class PermissionDSL
def initialize
@permissions = Hash.new { |h, k| h[k] = [] }
@current_role = nil
end
# DSL 方法:定義角色
def role(role_name, &block)
@current_role = role_name.to_sym
instance_eval(&block) if block_given?
@current_role = nil
self
end
# DSL 方法:授予權限
def can(action, resource, conditions = {})
raise "必須在 role 區塊內定義權限" unless @current_role
permission = {
action: action.to_sym,
resource: resource.to_sym,
conditions: conditions
}
# 如果有條件,將它轉換為 Lambda
if conditions[:if]
permission[:condition_lambda] = conditions[:if]
end
@permissions[@current_role] << permission
self
end
# DSL 方法:拒絕權限
def cannot(action, resource)
raise "必須在 role 區塊內定義權限" unless @current_role
@permissions[@current_role] << {
action: action.to_sym,
resource: resource.to_sym,
denied: true
}
self
end
# 檢查權限
def can?(role, action, resource, context = {})
role = role.to_sym
action = action.to_sym
resource = resource.to_sym
return false unless @permissions[role]
# 先檢查是否有明確的拒絕
denied = @permissions[role].any? do |perm|
perm[:denied] &&
perm[:action] == action &&
perm[:resource] == resource
end
return false if denied
# 檢查是否有授權
@permissions[role].any? do |perm|
next if perm[:denied]
next unless perm[:action] == action || perm[:action] == :manage
next unless perm[:resource] == resource || perm[:resource] == :all
# 如果有條件,檢查條件
if perm[:condition_lambda]
perm[:condition_lambda].call(context)
else
true
end
end
end
# 類別方法:建立權限系統
def self.define(&block)
dsl = new
dsl.instance_eval(&block)
dsl
end
end
# 定義權限規則
permissions = PermissionDSL.define do
role :student do
can :read, :course
can :read, :lesson
can :create, :enrollment
can :update, :profile, if: ->(context) {
context[:profile_id] == context[:current_user_id]
}
cannot :delete, :course # 明確禁止
end
role :instructor do
can :create, :course
can :update, :course, if: ->(context) {
context[:course].instructor_id == context[:current_user_id]
}
can :delete, :course, if: ->(context) {
course = context[:course]
course.instructor_id == context[:current_user_id] &&
course.enrollments.count == 0
}
can :manage, :lesson # 可以對課時執行任何操作
end
role :admin do
can :manage, :all # 管理員可以做任何事
end
end
# 測試權限系統
# 學生權限測試
context = { current_user_id: 1, profile_id: 1 }
puts permissions.can?(:student, :read, :course, context) # => true
puts permissions.can?(:student, :delete, :course, context) # => false
puts permissions.can?(:student, :update, :profile, context) # => true
# 講師權限測試
course = OpenStruct.new(instructor_id: 2, enrollments: [])
context = { current_user_id: 2, course: course }
puts permissions.can?(:instructor, :update, :course, context) # => true
puts permissions.can?(:instructor, :delete, :course, context) # => true
# 管理員權限測試
puts permissions.can?(:admin, :delete, :anything, {}) # => true
從其他程式語言轉換到 Ruby 和 Rails,最大的挑戰不是語法,而是思維模式的轉變。讓我分享一些關鍵的思維轉換點。
如果你來自 JavaScript 的世界,你可能習慣了回呼地獄和 Promise 鏈。在 Ruby 中,同步的程式碼流程讓邏輯更清晰。你不需要擔心 async/await
,因為 Ruby 的設計就是讓程式碼像說故事一樣自然流暢。當你真正需要非同步處理時,Rails 提供了 ActiveJob 這個優雅的解決方案。
如果你來自 Java 的世界,你可能會覺得失去了型別系統的保護。但請記住,Ruby 社群用完善的測試來彌補這個差異。在 Ruby 中,測試不是可選的,而是開發流程的核心。當你習慣了測試驅動開發,你會發現動態型別帶來的靈活性遠超過它的風險。
如果你來自 Python 的世界,你會發現 Ruby 的表達力更強。Ruby 相信「不只一種方法做事」,這給了你更多的創造空間。但這也意味著你需要學習社群的慣例,理解什麼是「Ruby Way」。
最重要的是,要擁抱 Ruby 的哲學:讓程式設計師快樂。當你發現自己在寫 Ruby 時會心一笑,當你驚嘆於程式碼的優雅,你就真正理解了 Ruby 的精髓。
今天是我們三十天旅程的第一天,但我們已經走了很遠。我們不僅搭建了開發環境,創建了真實的 Rails 專案,更重要的是,我們深入理解了 Ruby 語言的核心特性,以及這些特性如何支撐 Rails 的優雅設計。
我們理解了「一切皆物件」不只是一個概念,而是 Ruby 世界的基本法則。我們學會了使用 Block 來創造優雅的控制結構,用 Lambda 和 Proc 來封裝可重用的邏輯。我們深入探討了 Symbol 這個 Ruby 獨特的概念,理解了它在效能優化和程式碼表達上的重要性。
更重要的是,我們在實踐中應用了這些知識。我們建立了 Model,創建了 Controller,設計了路由,甚至實作了自己的 DSL。這不是紙上談兵的理論學習,而是真實的專案開發經驗。
回顧今天的學習,你已經掌握了構建 Rails 應用的基礎工具。你理解了 Block 如何讓程式碼變得優雅,Symbol 如何優化效能,Lambda 和 Proc 如何提供靈活性。你也看到了這些特性如何在 Rails 中發揮作用,從 Model 的 scope 到 Controller 的 callbacks,從路由的 DSL 到服務物件的設計。
這些知識將成為你接下來學習的基石。明天當我們深入 Rails 的專案結構時,你會看到今天學的概念如何體現在框架的每個角落。第二週當我們處理複雜的業務邏輯時,今天學的 Ruby 特性會成為你的得力工具。最終在建構完整的 LMS 系統時,這些基礎知識會幫助你寫出優雅而高效的程式碼。
在結束今天的學習之前,請確認你已經達成以下目標:
明天,我們將深入探討 Rails 的專案結構與設計哲學。如果說今天我們學習的是建築材料和工具,明天我們將學習如何設計整座建築。你會理解為什麼 Rails 的目錄是這樣組織的,每個資料夾背後隱含著什麼樣的設計理念。
我們會探討「約定優於配置」不只是減少設定檔,而是一種將二十年最佳實踐內建到框架中的智慧。你會學習 Rails 的自動載入機制如何運作,理解 Zeitwerk 如何改變了 Ruby 的程式碼組織方式。最重要的是,你會開始理解 Rails 的「魔法」其實都有清晰的原理。
準備好深入 Rails 的內部世界了嗎?明天,我們將一起揭開 Rails 優雅背後的秘密。記得帶著今天學到的 Ruby 知識,因為你會看到它們如何在 Rails 的每個角落發揮作用,共同編織出這個強大而優雅的框架。
今天只是開始,但這是一個堅實的開始。保持你的好奇心和熱情,Rails 的世界正等待著你去探索。明天見!